type TEvent = string | number | symbol
type TListener<Args extends any[] = any[]> = (...args: Args) => any

export type { TEvent, TListener }

/**
 * 发布订阅
 * @example
 * type TEventArgsMap = {
 *   ['change']: [e: { data: IData, prevData: IData }];
 *   ['beforeSend']: [e: { data: IData }];
 *   ['close']: [];
 * }
 * const emitter = new EventEmitter<TEventArgsMap>()
 */
export class EventEmitter<ArgsMap extends Record<TEvent, any[]>> {
  protected _listenersMap = new Map<TEvent, { fn: TListener; once: boolean }[]>()

  protected _addEventListener = (event: TEvent, listener: TListener, options?: { once: boolean }): (() => void) => {
    if (typeof listener !== 'function') return () => undefined

    const listeners = this._listenersMap.get(event) || []
    if (!listeners.find(({ fn }) => fn === listener)) {
      listeners.push({ fn: listener, once: !!options?.once })
      this._listenersMap.set(event, listeners)
    }

    return () => this.off(event, listener)
  }

  on = <E extends keyof ArgsMap>(event: E, listener: TListener<ArgsMap[E]>): (() => void) => {
    return this._addEventListener(event, listener)
  }

  once = <E extends keyof ArgsMap>(event: E, listener: TListener<ArgsMap[E]>): (() => void) => {
    return this._addEventListener(event, listener, { once: true })
  }

  off = <E extends keyof ArgsMap>(event: E, listener?: TListener<ArgsMap[E]>) => {
    const listeners = this._listenersMap.get(event)
    if (!listeners) return

    if (listener === undefined) {
      this._listenersMap.delete(event)
    } else if (typeof listener === 'function') {
      this._listenersMap.set(
        event,
        listeners.filter(({ fn }) => fn !== listener),
      )
    }
  }

  offAll = () => {
    this._listenersMap.clear()
  }

  emit = <E extends keyof ArgsMap>(event: E, ...args: ArgsMap[E]) => {
    this._listenersMap.get(event)?.forEach(({ fn, once }) => {
      try {
        if (once) this.off(event, fn)
        fn?.(...args)
      } catch (err) {
        setTimeout(() => {
          throw err
        })
      }
    })
  }
}
